/*
 * Copyright (C) 2015 Daniel Uppenkamp
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.maurersmarbles;

import com.google.common.base.Preconditions;
import java.util.LinkedHashSet;
import java.util.Set;

/**
 *
 * @author Daniel Uppenkamp
 */
public class GameLogic {

    /**
     * The number of cards to deal to each player when the deck is full.
     */
    public static final int NUMBER_OF_CARDS_TO_DEAL_WITH_FULL_DECK = 5;
    /**
     * The number of marbles each player has.
     */
    public static final int NUMBER_OF_MARBLES_PER_PLAYER = 4;
    /**
     * The number of players.
     */
    public static final int NUMBER_OF_PLAYERS = 4;
    /**
     * The marble colors.
     */
    protected static final int[] MARBLE_COLORS = {RGB.BLUE, RGB.YELLOW, RGB.RED, RGB.GREEN};

    /**
     * Updates the game state by applying the given card being played by the
     * given player number.
     *
     * @param gameState the game state
     * @param playerNumber the player number
     * @param card the card
     */
    public static void applyCardBeingPlayed(GameState gameState, int playerNumber, Card card) {
        Preconditions.checkNotNull(gameState, "Null game state");
        gameState.getPlayers().get(playerNumber).getCards().remove(card);
    }

    /**
     * Updates the game state by applying the given move.
     *
     * @param gameState the game state
     * @param move the move to apply
     */
    public static void applyMove(GameState gameState, Move move) {

        Preconditions.checkNotNull(gameState, "Null game state");
        Preconditions.checkNotNull(move, "Null move");

        // apply the move to update the marble
        Marble marble = gameState.getPlayers().get(move.getPlayerNumber()).getMarble(move.getMarbleNumber());
        marble.setBoardIndex(move.getNewMarbleBoardIndex());

    }

    /**
     * Updates the game state by applying the given play for the given player
     * number.
     *
     * @param gameState the game state
     * @param playerNumber the player number
     * @param play the play to apply
     */
    public static void applyPlay(GameState gameState, int playerNumber, Play play) {

        Preconditions.checkNotNull(play, "Null play");

        // apply the card being played
        applyCardBeingPlayed(gameState, playerNumber, play.getCard());

        // apply the moves that make up the play
        for (Move move : play.getMoves()) {
            applyMove(gameState, move);
        }

    }

    /**
     * Returns whether or not moving from the given old board index to the given
     * new board index crosses the given board index.
     *
     * @param oldBoardIndex the old board index
     * @param newBoardIndex the new board index
     * @param boardIndex the board index to test being crossed
     * @return whether or not moving from the given old board index to the given
     * new board index crosses the given board index
     */
    protected static boolean crossesBoardIndex(int oldBoardIndex, int newBoardIndex, int boardIndex) {
        return (oldBoardIndex < boardIndex && newBoardIndex > boardIndex
                || oldBoardIndex > boardIndex && newBoardIndex < boardIndex);
    }

    /**
     * Deals the cards to each player using the given game state.
     *
     * @param gameState the game state
     */
    public static void dealCards(GameState gameState) {

        Preconditions.checkNotNull(gameState, "Null game state");

        // advance the dealer
        gameState.setDealer(getNextPlayerNumber(gameState.getDealer()));

        // get the card deck
        CardDeck cardDeck = gameState.getCardDeck();
        if (cardDeck == null || cardDeck.isEmpty()) {
            // the card deck is not set or empty, create a new shuffled deck
            cardDeck = new CardDeck(true);
            gameState.setCardDeck(cardDeck);
        }

        // get the number of cards to deal to each player
        int numberOfCardsPerPlayer = getNumberOfCardsToDealPerPlayer(gameState);

        // deal the cards to each player
        for (int i = 0; i < numberOfCardsPerPlayer; i++) {

            for (int j = 1; j <= GameLogic.NUMBER_OF_PLAYERS; j++) {

                int playerNumber = gameState.getDealer() + j;
                if (playerNumber >= GameLogic.NUMBER_OF_PLAYERS) {
                    playerNumber -= GameLogic.NUMBER_OF_PLAYERS;
                }

                gameState.getPlayers().get(playerNumber).getCards().add(cardDeck.nextCard());

            }

        }

    }

    /**
     * Returns the best play based on the given game state.
     *
     * @param gameState the game state
     * @param playerNumber the player number
     * @param plays the plays
     * @return the best play based on the given game state
     */
    public static Play getBestPlay(GameState gameState, int playerNumber, Set<Play> plays) {

        Preconditions.checkNotNull(plays, "Null plays");

        int maxScore = Integer.MIN_VALUE;
        Play bestPlay = null;
        for (Play play : plays) {
            int score = GameLogic.getScoreForPlay(gameState, playerNumber, play);
            if (score > maxScore) {
                maxScore = score;
                bestPlay = play;
            }
        }

        return bestPlay;

    }

    /**
     * Returns the board index for an empty start space for the given player.
     *
     * @param player the player
     * @return the board index for an empty start space for the given player
     */
    protected static int getEmptyStartSpaceBoardIndex(Player player) {

        Preconditions.checkNotNull(player, "Null player");

        int playerNumber = player.getPlayerNumber();

        for (int i = Board.BOARD_INDEX_START_MIN[playerNumber]; i <= Board.BOARD_INDEX_START_MAX[playerNumber]; i++) {

            boolean spaceIsEmpty = true;

            for (Marble marble : player.getMarbles()) {
                if (marble.getBoardIndex() == i) {
                    spaceIsEmpty = false;
                    break;
                }
            }

            if (spaceIsEmpty) {
                return i;
            }

        }

        return -1;

    }

    /**
     * Returns the marble color for the given player number.
     *
     * @param gameState the game state
     * @param playerNumber the player number
     * @return the marble color for the given player number
     */
    public static int getMarbleColorForPlayer(GameState gameState, int playerNumber) {
        Preconditions.checkNotNull(gameState, "Null game state");
        return getMarbleColorForPlayer(gameState.getUserMarbleColorIndex(), playerNumber);
    }

    /**
     * Returns the marble color for the given player number.
     *
     * @param userMarbleColorIndex the user's marble color index
     * @param playerNumber the player number
     * @return the marble color for the given player number
     */
    public static int getMarbleColorForPlayer(int userMarbleColorIndex, int playerNumber) {
        int marbleColorIndex = userMarbleColorIndex + playerNumber;
        if (marbleColorIndex >= NUMBER_OF_PLAYERS) {
            marbleColorIndex -= NUMBER_OF_PLAYERS;
        }
        return MARBLE_COLORS[marbleColorIndex];
    }

    /**
     * Returns the marble that is in the space at the given board index, null if
     * there is not a marble
     *
     * @param gameState the game state
     * @param boardIndex the board index of the space
     * @return the marble that is in the space at the given board index, null if
     * there is not a marble
     */
    protected static Marble getMarbleInSpace(GameState gameState, int boardIndex) {

        Preconditions.checkNotNull(gameState, "Null game state");

        for (Player player : gameState.getPlayers()) {
            for (Marble marble : player.getMarbles()) {
                if (marble.getBoardIndex() == boardIndex) {
                    return marble;
                }
            }
        }

        return null;

    }

    /**
     * Returns the next player number based on the given player number.
     *
     * @param playerNumber the current player number
     * @return the next player number based on the given player number
     */
    public static int getNextPlayerNumber(int playerNumber) {

        int nextPlayerNumber = playerNumber + 1;
        if (nextPlayerNumber >= NUMBER_OF_PLAYERS) {
            nextPlayerNumber = 0;
        }

        return nextPlayerNumber;

    }

    /**
     * Returns the number of cards to deal to each player.
     *
     * @param gameState the game state
     * @return the number of cards to deal to each player
     */
    public static int getNumberOfCardsToDealPerPlayer(GameState gameState) {
        Preconditions.checkNotNull(gameState, "Null game state");
        return NUMBER_OF_CARDS_TO_DEAL_WITH_FULL_DECK - (gameState.getCardDeck().isFull() ? 0 : 1);
    }

    /**
     * Returns the number of marbles in a home space for the given player.
     *
     * @param player the player
     * @return the number of marbles in a home space for the given player
     */
    protected static int getNumberOfMarblesInHomeSpace(Player player) {

        Preconditions.checkNotNull(player, "Null player");

        int count = 0;

        for (Marble marble : player.getMarbles()) {
            if (isMarbleInHomeSpace(marble)) {
                count++;
            }
        }

        return count;

    }

    /**
     * Returns the number of marbles in a start space for the given player.
     *
     * @param player the player
     * @return the number of marbles in a start space for the given player
     */
    protected static int getNumberOfMarblesInStartSpace(Player player) {

        Preconditions.checkNotNull(player, "Null player");

        int count = 0;

        for (Marble marble : player.getMarbles()) {
            if (isMarbleInStartSpace(marble)) {
                count++;
            }
        }

        return count;

    }

    /**
     * Returns the number of marbles that are on the board for the given player.
     *
     * @param player the player
     * @return the number of marbles that are on the board for the given player
     */
    protected static int getNumberOfMarblesOnBoard(Player player) {
        return NUMBER_OF_MARBLES_PER_PLAYER - getNumberOfMarblesInStartSpace(player);
    }

    /**
     * Returns the partner number for the given player number.
     *
     * @param playerNumber the player number
     * @return the partner number for the given player number
     */
    public static int getPartnerNumber(int playerNumber) {

        switch (playerNumber) {
            case 0:
                return 2;
            case 1:
                return 3;
            case 2:
                return 0;
            case 3:
                return 1;
        }

        return -1;

    }

    /**
     * Returns the play for the given player number, card, and marbles, or null
     * if there is not a valid play.
     *
     * @param gameState the game state
     * @param playerNumber the player number
     * @param card the card
     * @param marble1 the first marble
     * @param marble2 the second marble
     * @param splitValue1 the split value
     * @return the play for the given player number, card, and marbles, or null
     * if there is not a valid play
     */
    public static Play getPlay(GameState gameState, int playerNumber, Card card, Marble marble1, Marble marble2, int splitValue1) {

        Preconditions.checkNotNull(card, "Null card");

        switch (card.getId()) {

            case Card.CARD_ID_A:
                return getPlayForCardAceAndMarble(gameState, playerNumber, card, marble1);

            case Card.CARD_ID_K:
                return getPlayForCardKingAndMarble(gameState, playerNumber, card, marble1);

            case Card.CARD_ID_J:
                return getPlayForCardJackAndMarble(gameState, playerNumber, card, marble1, marble2);

            case Card.CARD_ID_7:
                return getPlayForCard7AndMarble(gameState, playerNumber, card, marble1, marble2, splitValue1);

            default:
                return getPlayForCardAndMarble(gameState, playerNumber, card, marble1);

        }

    }

    /**
     * Returns the play for the given player number, card and marble, or null if
     * there is not a valid play.
     *
     * @param gameState the game state
     * @param playerNumber the player number
     * @param card the card
     * @param marble the marble
     * @return the play for the given player number, card, and marble, or null
     * if there is not a valid play
     */
    protected static Play getPlayForCardAndMarble(GameState gameState, int playerNumber, Card card, Marble marble) {

        Preconditions.checkNotNull(gameState, "Null game state");
        Preconditions.checkNotNull(card, "Null card");
        Preconditions.checkNotNull(marble, "Null marble");

        // check to make sure the given marble is our own or our partners
        if (marble.getPlayerNumber() != playerNumber
                && marble.getPlayerNumber() != getPartnerNumber(playerNumber)) {
            // cannot move the marble with the card
            return null;
        }

        Player player = gameState.getPlayers().get(marble.getPlayerNumber());
        Player partner = gameState.getPlayers().get(getPartnerNumber(marble.getPlayerNumber()));

        int marbleBoardIndex = marble.getBoardIndex();

        // check to see if the marble is on the board
        if (!isMarbleOnBoard(marble)) {
            // cannot move the marble with the card
            return null;
        }

        // check to see if the marble is in a home space
        if (isMarbleInHomeSpace(marble)) {

            // cannot play a 4 (which goes backwards)
            if (card.getValue() < 0) {
                return null;
            }

            // cannot go beyond home spaces
            if (marbleBoardIndex + card.getValue() > Board.BOARD_INDEX_HOME_MAX[marble.getPlayerNumber()]) {
                return null;
            }

            // cannot land on or pass another marble in the home spaces
            for (int j = marbleBoardIndex + 1; j <= marbleBoardIndex + card.getValue(); j++) {
                if (isPlayerMarbleInSpace(player, j)) {
                    return null;
                }
            }

            // the marble can be moved with the card, create and add the play
            Play play = new Play(card);
            play.addMove(new Move(marble, marbleBoardIndex + card.getValue()));
            return play;

        }

        // check to see if we passed the marble's home entry space
        if (marbleBoardIndex <= Board.BOARD_INDEX_HOME_ENTRY[marble.getPlayerNumber()]
                && marbleBoardIndex + card.getValue() > Board.BOARD_INDEX_HOME_ENTRY[marble.getPlayerNumber()]) {

            // calculate how many spaces the card moves the marble beyond the home entry
            int spacesBeyondHomeEntry = marbleBoardIndex + card.getValue() - Board.BOARD_INDEX_HOME_ENTRY[marble.getPlayerNumber()];

            // cannot move more than 4 spaces in the home spaces
            if (spacesBeyondHomeEntry > 4) {
                // the marble cannot be moved with the card
                return null;
            }

            // cannot land on or pass another marble in the home spaces
            for (int j = 0; j < spacesBeyondHomeEntry; j++) {
                if (isPlayerMarbleInSpace(player, Board.BOARD_INDEX_HOME_MIN[marble.getPlayerNumber()] + j)) {
                    return null;
                }
            }

            // this marble can be moved with the card, create and add the play
            Play play = new Play(card);
            play.addMove(new Move(marble, Board.BOARD_INDEX_HOME_MIN[marble.getPlayerNumber()] + (spacesBeyondHomeEntry - 1)));
            return play;

        }

        /**
         * Check to see if we wrapped the board index. The board index starts
         * with 0 at player 0's safe spot.
         */
        boolean wrappedBoardIndex = false;
        int newMarbleBoardIndex = marbleBoardIndex + card.getValue();
        if (newMarbleBoardIndex >= Board.NUMBER_OF_BOARD_SPACES) {
            newMarbleBoardIndex = newMarbleBoardIndex - Board.NUMBER_OF_BOARD_SPACES;
            wrappedBoardIndex = true;
        }
        else if (newMarbleBoardIndex < 0) {
            newMarbleBoardIndex = newMarbleBoardIndex + Board.NUMBER_OF_BOARD_SPACES;
            wrappedBoardIndex = true;
        }

        // check to see if we landed on our marble or our partner's marble
        if (isPlayerMarbleInSpace(player, newMarbleBoardIndex)
                || isPlayerMarbleInSpace(partner, newMarbleBoardIndex)) {
            // this marble cannot be moved with the card
            return null;
        }

        // check to see if we landed on a safe marble
        for (Player tmpPlayer : gameState.getPlayers()) {
            if (newMarbleBoardIndex == Board.BOARD_INDEX_SAFE[tmpPlayer.getPlayerNumber()]
                    && hasMarbleInSafeSpace(tmpPlayer)) {
                // the marble would land on a safe marble which is not allowed
                return null;
            }
        }

        // check to see if we passed a safe marble
        if (!isMarbleInSafeSpace(marble)) {

            for (Player tmpPlayer : gameState.getPlayers()) {

                // check to see if the player has a marble in the safe space
                if (hasMarbleInSafeSpace(tmpPlayer)) {

                    /**
                     * The player has a marble in their safe space. Check to see
                     * if this marble crosses the player's safe space. The board
                     * index starts with 0 at player 0's safe space so we need
                     * to do an additional check to handle the board index
                     * wrapping around.
                     */
                    if (tmpPlayer.getPlayerNumber() != 0) {

                        if (crossesBoardIndex(marbleBoardIndex, marbleBoardIndex + card.getValue(), Board.BOARD_INDEX_SAFE[tmpPlayer.getPlayerNumber()])) {
                            // the marble crossed a player's safe marble which is not allowed
                            return null;
                        }

                    }
                    else if (wrappedBoardIndex) {
                        // the marble crossed player 0's safe marble which is not allowed
                        return null;
                    }

                }

            }

        }

        // the marble can be moved with the card, create the play
        Play play = new Play(card);

        // create the move for moving the marble to the new board index
        play.addMove(new Move(marble, newMarbleBoardIndex));

        /**
         * Check to see if there was a marble in the new space, if there was,
         * add a move to the play to move it back to a start space.
         */
        Marble previousMarble = getMarbleInSpace(gameState, newMarbleBoardIndex);
        if (previousMarble != null) {
            play.addMove(new Move(previousMarble, getEmptyStartSpaceBoardIndex(gameState.getPlayers().get(previousMarble.getPlayerNumber()))));
        }

        return play;

    }

    /**
     * Returns the play for the given player number, card, and marbles, or null
     * if there is not a valid play.
     *
     * @param gameState the game state
     * @param playerNumber the player number
     * @param card the card
     * @param marble1 the first marble
     * @param marble2 the second marble
     * @param splitValue1 the number of spaces to move the first marble
     * @return the play for the given player number, card, and marbles, or null
     * if there is not a valid play
     */
    protected static Play getPlayForCard7AndMarble(GameState gameState, int playerNumber, Card card, Marble marble1, Marble marble2, int splitValue1) {

        // check to see if we are moving 1 marble 7 spaces
        if (splitValue1 == 7) {
            // return the play for moving 1 marble 7 spaces
            return getPlayForCardAndMarble(gameState, playerNumber, card, marble1);
        }

        // check to make sure the marbles were provided
        if (marble1 == null || marble2 == null) {
            return null;
        }

        // check to make sure the given marbles are our own or our partners
        if ((marble1.getPlayerNumber() != playerNumber
                && marble1.getPlayerNumber() != getPartnerNumber(playerNumber))
                || (marble2.getPlayerNumber() != playerNumber
                && marble2.getPlayerNumber() != getPartnerNumber(playerNumber))) {
            // cannot move the marbles with the card
            return null;
        }

        // check to make sure we are not trying to move the same marble
        if (marble1.equals(marble2)) {
            return null;
        }

        // calculate the value for the second move
        int splitValue2 = 7 - splitValue1;

        // create temp cards for the split values
        Card tmpCard1 = new Card(Card.CARD_ID_SPLIT, card.getSuit(), splitValue1);
        Card tmpCard2 = new Card(Card.CARD_ID_SPLIT, card.getSuit(), splitValue2);

        // get the play for the first move
        Play tmpPlay1 = getPlayForCardAndMarble(gameState, playerNumber, tmpCard1, marble1);
        if (tmpPlay1 != null) {

            /**
             * In order to determine what plays are valid for the second portion
             * of the split, we first need to apply the first play. To do this,
             * we will create a new game state and apply the first play, and
             * then use the new game state to determine what plays are valid for
             * the second portion.
             */
            GameState tmpGameState = new GameState(gameState);

            /**
             * Note this function will try to remove the tmp card that we
             * created but we don't care for this purpose. If it turns out to be
             * a valid play, a new play is created later with the correct card.
             */
            applyPlay(tmpGameState, playerNumber, tmpPlay1);

            // get the play for the second move
            Play tmpPlay2 = getPlayForCardAndMarble(tmpGameState, playerNumber, tmpCard2, marble2);
            if (tmpPlay2 != null) {

                // we can move both marbles, create the play
                Play play = new Play(card);

                // add all of the moves from the first play
                play.addMoves(tmpPlay1.getMoves());

                // add all of the moves from the second play
                play.addMoves(tmpPlay2.getMoves());

                return play;

            }

        }

        return null;

    }

    /**
     * Returns the play for the given player number, card, and marble, or null
     * if there is not a valid play.
     *
     * @param gameState the game state
     * @param playerNumber the player number
     * @param card the card
     * @param marble the marble
     * @return the play for the given player number, card, and marble, or null
     * if there is not a valid play
     */
    protected static Play getPlayForCardAceAndMarble(GameState gameState, int playerNumber, Card card, Marble marble) {

        // first check to see if there is a play for moving out of a start space
        Play play = getPlayForCardKingAndMarble(gameState, playerNumber, card, marble);
        if (play != null) {
            return play;
        }

        // check to see if there is a play to move on the board
        play = getPlayForCardAndMarble(gameState, playerNumber, card, marble);

        return play;

    }

    /**
     * Returns the play for the given player number, card, and marbles, or null
     * if there is not a valid play.
     *
     * @param gameState the game state
     * @param playerNumber the player number
     * @param card the card
     * @param marble1 the first marble to swap
     * @param marble2 the second marble to swap
     * @return the play for the given player number, card, and marbles, or null
     * if there is not a valid play
     */
    protected static Play getPlayForCardJackAndMarble(GameState gameState, int playerNumber, Card card, Marble marble1, Marble marble2) {

        // check to make sure marble1 and marble2 is not null
        if (marble1 == null || marble2 == null) {
            return null;
        }

        // check to make sure marble1 and marble2 do not belong to the same player
        if (marble1.getPlayerNumber() == marble2.getPlayerNumber()) {
            // we cannot swap marbles for the same player
            return null;
        }

        // check to make sure marble1 or marble2 belongs to us or our partner
        if (marble1.getPlayerNumber() != playerNumber
                && marble1.getPlayerNumber() != getPartnerNumber(playerNumber)
                && marble2.getPlayerNumber() != playerNumber
                && marble2.getPlayerNumber() != getPartnerNumber(playerNumber)) {
            // cannot move the marble with the card
            return null;
        }

        // check to make sure marble1 is on the board and not in a home space
        if (!isMarbleOnBoard(marble1) || isMarbleInHomeSpace(marble1)) {
            // marble1 cannot be moved with the card
            return null;
        }

        // check to make sure marble2 is on the board and not in a home space
        if (!isMarbleOnBoard(marble2) || isMarbleInHomeSpace(marble2)) {
            // marble2 cannot be moved with the card
            return null;
        }

        // we cannot swap a marble that is in a safe space unless it is our own or our partners
        if (isMarbleInSafeSpace(marble1)) {
            if (marble1.getPlayerNumber() != playerNumber
                    && marble1.getPlayerNumber() != getPartnerNumber(playerNumber)) {
                // marble1 cannot be moved with the card
                return null;
            }
        }
        if (isMarbleInSafeSpace(marble2)) {
            if (marble2.getPlayerNumber() != playerNumber
                    && marble2.getPlayerNumber() != getPartnerNumber(playerNumber)) {
                // marble2 cannot be moved with the card
                return null;
            }
        }

        // we can swap the marbles, create the play
        Play play = new Play(card);

        // create the move to move marble1 to marbles2's board index
        play.addMove(new Move(marble1, marble2.getBoardIndex()));

        // create the move to move marble2 to marble1's board index
        play.addMove(new Move(marble2, marble1.getBoardIndex()));

        return play;

    }

    /**
     * Returns the play for the given player number, card and marble, or null if
     * there is not a valid play.
     *
     * @param gameState the game state
     * @param playerNumber the player number
     * @param card the card
     * @param marble the marble
     * @return the play for the given player number, card and marble, or null if
     * there is not a valid play
     */
    protected static Play getPlayForCardKingAndMarble(GameState gameState, int playerNumber, Card card, Marble marble) {

        Preconditions.checkNotNull(gameState, "Null game state");
        Preconditions.checkNotNull(marble, "Null marble");

        // check to make sure the given marble is our own or our partners
        if (marble.getPlayerNumber() != playerNumber
                && marble.getPlayerNumber() != getPartnerNumber(playerNumber)) {
            // cannot move the marble with the card
            return null;
        }

        Player player = gameState.getPlayers().get(marble.getPlayerNumber());
        Player partner = gameState.getPlayers().get(getPartnerNumber(marble.getPlayerNumber()));

        // 1) Marble must be in a start space
        // 2) Cannot have one of my marbles in my safe spot
        // 3) Partner cannot have a marble in my safe spot
        if (isMarbleInStartSpace(marble)
                && !hasMarbleInSafeSpace(player)
                && !hasMarbleInSpace(partner, Board.BOARD_INDEX_SAFE[marble.getPlayerNumber()])) {

            // get the marble that is currently in the safe space
            Marble previousMarble = getMarbleInSpace(gameState, Board.BOARD_INDEX_SAFE[marble.getPlayerNumber()]);

            // create the play
            Play play = new Play(card);

            // create the move for moving the marble into the safe space
            play.addMove(new Move(marble.getPlayerNumber(), marble.getMarbleNumber(), Board.BOARD_INDEX_SAFE[marble.getPlayerNumber()]));

            /**
             * Check to see if there was a marble in the safe space, if there
             * was, add a move to the play to move it back to a start space.
             */
            if (previousMarble != null) {
                play.addMove(new Move(previousMarble, getEmptyStartSpaceBoardIndex(gameState.getPlayers().get(previousMarble.getPlayerNumber()))));
            }

            return play;

        }

        return null;

    }

    /**
     * Returns the valid plays for the given player number using the given game
     * state.
     *
     * @param gameState the game state
     * @param playerNumber the player number
     * @return the valid plays for the given player number using the given game
     * state
     */
    public static Set<Play> getPlays(GameState gameState, int playerNumber) {

        Preconditions.checkNotNull(gameState, "Null game state");

        Set<Play> plays = new LinkedHashSet();

        // loop through the list of cards
        for (Card card : gameState.getPlayers().get(playerNumber).getCards()) {

            // get plays for the given player number and their partner
            for (int currentPlayerNumber : new int[]{playerNumber, getPartnerNumber(playerNumber)}) {

                switch (card.getId()) {

                    case Card.CARD_ID_A:
                        plays.addAll(getPlaysForCardKing(gameState, currentPlayerNumber, card));
                        plays.addAll(getPlaysForCard(gameState, currentPlayerNumber, card));
                        break;

                    case Card.CARD_ID_K:
                        plays.addAll(getPlaysForCardKing(gameState, currentPlayerNumber, card));
                        break;

                    case Card.CARD_ID_J:
                        plays.addAll(getPlaysForCardJack(gameState, currentPlayerNumber, card));
                        break;

                    case Card.CARD_ID_7:
                        plays.addAll(getPlaysForCard7(gameState, currentPlayerNumber, card));
                        break;

                    default:
                        plays.addAll(getPlaysForCard(gameState, currentPlayerNumber, card));
                        break;

                }

            }

        }

        return plays;

    }

    /**
     * Returns the plays for the given player number and the given card.
     *
     * @param gameState the game state
     * @param playerNumber the player number
     * @param card the card
     * @return the plays for the given player number and the given card
     */
    protected static Set<Play> getPlaysForCard(GameState gameState, int playerNumber, Card card) {

        Preconditions.checkNotNull(gameState, "Null game state");

        Set<Play> plays = new LinkedHashSet();

        // add the play for each marble
        for (Marble marble : gameState.getPlayers().get(playerNumber).getMarbles()) {
            Play play = getPlayForCardAndMarble(gameState, playerNumber, card, marble);
            if (play != null) {
                plays.add(play);
            }
        }

        return plays;

    }

    /**
     * Returns the plays for the given player number and the given card.
     *
     * @param gameState the game state
     * @param playerNumber the player number
     * @param card the card
     * @return the plays for the given player number and the given card
     */
    protected static Set<Play> getPlaysForCard7(GameState gameState, int playerNumber, Card card) {

        Preconditions.checkNotNull(gameState, "Null game state");

        Set<Play> plays = new LinkedHashSet();

        // add the plays for moving each marble 7 spaces
        for (Marble marble : gameState.getPlayers().get(playerNumber).getMarbles()) {
            Play play = getPlayForCard7AndMarble(gameState, playerNumber, card, marble, null, 7);
            if (play != null) {
                plays.add(play);
            }
        }

        // get the plays for splitting the 7 and moving 2 marbles
        for (int splitValue1 = 1; splitValue1 <= 6; splitValue1++) {

            // loop through all of our marbles
            for (Marble marble1 : gameState.getPlayers().get(playerNumber).getMarbles()) {

                // get plays for us and our partner
                for (int currentPlayerNumber : new int[]{playerNumber, getPartnerNumber(playerNumber)}) {

                    // try to swap with our other marbles and our partners marbles
                    for (Marble marble2 : gameState.getPlayers().get(currentPlayerNumber).getMarbles()) {

                        Play play = getPlayForCard7AndMarble(gameState, playerNumber, card, marble1, marble2, splitValue1);
                        if (play != null) {
                            plays.add(play);
                        }

                    }

                }

            }

        }

        return plays;

    }

    /**
     * Returns the plays for the given player number and the given card.
     *
     * @param gameState the game state
     * @param playerNumber the player number
     * @param card the card
     * @return the plays for the given player number and the given card
     */
    protected static Set<Play> getPlaysForCardJack(GameState gameState, int playerNumber, Card card) {

        Preconditions.checkNotNull(gameState, "Null game state");

        Set<Play> plays = new LinkedHashSet();

        Player player = gameState.getPlayers().get(playerNumber);

        // check each marble to see if it can be moved with this card
        for (Marble myMarble : player.getMarbles()) {

            // loop over the other players to see what marbles we can swap with
            for (Player otherPlayer : gameState.getPlayers()) {

                // check to make sure this isn't the same player
                if (otherPlayer == player) {
                    // we cannot swap our marble with our own marble
                    continue;
                }

                // check to see what marbles we can swap with
                for (Marble theirMarble : otherPlayer.getMarbles()) {

                    Play play = getPlayForCardJackAndMarble(gameState, playerNumber, card, myMarble, theirMarble);
                    if (play != null) {
                        plays.add(play);
                    }

                }

            }

        }

        return plays;

    }

    /**
     * Returns the plays for the given player number and the given card.
     *
     * @param gameState the game state
     * @param playerNumber the player number
     * @param card the card
     * @return the plays for the given player number and the given card
     */
    protected static Set<Play> getPlaysForCardKing(GameState gameState, int playerNumber, Card card) {

        Preconditions.checkNotNull(gameState, "Null game state");

        Set<Play> plays = new LinkedHashSet();

        // add the play for each marble
        for (Marble marble : gameState.getPlayers().get(playerNumber).getMarbles()) {
            Play play = getPlayForCardKingAndMarble(gameState, playerNumber, card, marble);
            if (play != null) {
                plays.add(play);
            }
        }

        return plays;

    }

    /**
     * Returns the score for the given marble.
     *
     * @param marble the marble
     * @return the score for the given marble
     */
    public static int getScoreForMarble(Marble marble) {

        Preconditions.checkNotNull(marble, "Null marble");

        if (isMarbleInStartSpace(marble)) {
            /**
             * TODO Need to decide if we want to increase the score for a marble
             * in a start space. Doing so would create a bigger reward for
             * getting marbles on the board. If no, we can combine this if
             * statement with the if statement below checking to see if we are 1
             * space before the safe space which requires the same number of
             * moves.
             */
            /**
             * The marble is in a start space. It will require 1 move to get to
             * the safe space and then we need to go all of the way around the
             * board.
             */
            return Board.NUMBER_OF_BOARD_SPACES + Board.NUMBER_OF_HOME_SPACES - 1;
        }
        else if (marble.getBoardIndex() == (Board.BOARD_INDEX_HOME_ENTRY[marble.getPlayerNumber()] + 1)) {
            /**
             * The marble is in the space before the safe space. It will require
             * 1 move to get to the safe space and then we need to go all of the
             * way around the board.
             */
            return Board.NUMBER_OF_BOARD_SPACES + Board.NUMBER_OF_HOME_SPACES - 1;
        }
        else if (isMarbleInHomeSpace(marble)) {
            /**
             * TODO Does this logic work? Or do we need to account for marbles
             * that are already in the home spaces. If we leave it the way it
             * is, the lowest score a player can have is 6.
             */
            /**
             * The marble is in a home space. Determine how many more spaces we
             * need to move in the home spaces.
             */
            return Board.BOARD_INDEX_HOME_MAX[marble.getPlayerNumber()] - marble.getBoardIndex();
        }
        else {

            if (marble.getPlayerNumber() == 0) {
                /**
                 * Player 0's safe space starts with 0 on the board. We can
                 * calculate the score by the number of spaces needed to go
                 * around minus the marble board index.
                 */
                return (Board.NUMBER_OF_BOARD_SPACES + Board.NUMBER_OF_HOME_SPACES - 2) - marble.getBoardIndex();
            }
            else {
                /**
                 * For the other players, we need to account for the board index
                 * wrapping from 55 to 0 at player 0's safe space.
                 */
                if (marble.getBoardIndex() > Board.BOARD_INDEX_HOME_ENTRY[marble.getPlayerNumber()]) {
                    /**
                     * The marble has not crossed board index 0 yet. We need to
                     * count the number of spaces to get to board index 0 and
                     * add the number of spaces to get to the home entry and
                     * home spaces.
                     */
                    return (Board.NUMBER_OF_BOARD_SPACES - marble.getBoardIndex()) + Board.BOARD_INDEX_HOME_ENTRY[marble.getPlayerNumber()] + Board.NUMBER_OF_HOME_SPACES;
                }
                else {
                    /**
                     * The marble is beyond the board index 0. We just need to
                     * count the number of spaces to get to the home entry and
                     * home spaces.
                     */
                    return (Board.BOARD_INDEX_HOME_ENTRY[marble.getPlayerNumber()] - marble.getBoardIndex()) + Board.NUMBER_OF_HOME_SPACES;
                }

            }

        }

    }

    /**
     * Returns the score for the given play.
     *
     * @param gameState the game state
     * @param playerNumber the player number
     * @param play the play
     * @return the score for the given play
     */
    public static int getScoreForPlay(GameState gameState, int playerNumber, Play play) {

        // get the scores before the play
        int ourScoreBefore = getScoreForTeam(gameState, playerNumber);
        int theirScoreBefore = getScoreForTeam(gameState, getNextPlayerNumber(playerNumber));

        // create a temp game state to apply the play in
        GameState tmpGameState = new GameState(gameState);
        GameLogic.applyPlay(tmpGameState, playerNumber, play);

        // get the scores after the play
        int ourScoreAfter = getScoreForTeam(tmpGameState, playerNumber);
        int theirScoreAfter = getScoreForTeam(tmpGameState, getNextPlayerNumber(playerNumber));

        return (ourScoreBefore - ourScoreAfter) - (theirScoreBefore - theirScoreAfter);

    }

    /**
     * Returns the score for the given player.
     *
     * @param player the player
     * @return the score for the given player
     */
    public static int getScoreForPlayer(Player player) {

        Preconditions.checkNotNull(player, "Null player");

        int score = 0;
        for (Marble marble : player.getMarbles()) {
            score += getScoreForMarble(marble);
        }

        return score;

    }

    /**
     * Returns the score for the team with the given player number.
     *
     * @param gameState the game state
     * @param playerNumber the player number
     * @return the score for the team with the given player number
     */
    public static int getScoreForTeam(GameState gameState, int playerNumber) {

        Preconditions.checkNotNull(gameState, "Null game state");

        Player player = gameState.getPlayers().get(playerNumber);
        Player partner = gameState.getPlayers().get(getPartnerNumber(playerNumber));

        return getScoreForPlayer(player) + getScoreForPlayer(partner);

    }

    /**
     * Returns the winning player number (either 0 or 1) using the given game
     * state, or -1 if there is no winner.
     *
     * @param gameState the game state
     * @return the winning player number (either 0 or 1) using the given game
     * state, or -1 if there is no winner
     */
    public static int getWinningPlayerNumber(GameState gameState) {

        Preconditions.checkNotNull(gameState, "Null game state");

        for (int i = 0; i < NUMBER_OF_PLAYERS / 2; i++) {

            Player player = gameState.getPlayers().get(i);
            Player partner = gameState.getPlayers().get(getPartnerNumber(i));

            // check to see if the player and their partner have all of their marbles in a home space
            if (getNumberOfMarblesInHomeSpace(player) == NUMBER_OF_MARBLES_PER_PLAYER
                    && getNumberOfMarblesInHomeSpace(partner) == NUMBER_OF_MARBLES_PER_PLAYER) {
                return i;
            }

        }

        return -1;

    }
    
    /**
     * Returns whether or not the given player has a marble in the given space.
     *
     * @param player the player
     * @param boardIndex the board index of the space
     * @return whether or not the given player has a marble in the given space
     */
    protected static boolean hasMarbleInSpace(Player player, int boardIndex) {

        Preconditions.checkNotNull(player, "Null player");

        for (Marble marble : player.getMarbles()) {
            if (marble.getBoardIndex() == boardIndex) {
                return true;
            }
        }

        return false;

    }

    /**
     * Returns whether or not the given player has a marble in the safe space.
     *
     * @param player the player
     * @return whether or not the given player has a marble in the safe space
     */
    protected static boolean hasMarbleInSafeSpace(Player player) {

        Preconditions.checkNotNull(player, "Null player");

        // TODO Need to account for a non safe marble in the safe space
        return hasMarbleInSpace(player, Board.BOARD_INDEX_SAFE[player.getPlayerNumber()]);

    }

    /**
     * Returns whether or not the given player has a marble in a start space.
     *
     * @param player the player
     * @return whether or not the given player has a marble in a start space
     */
    protected static boolean hasMarbleInStartSpace(Player player) {

        Preconditions.checkNotNull(player, "Null player");

        for (Marble marble : player.getMarbles()) {
            if (isMarbleInStartSpace(marble)) {
                return true;
            }
        }

        return false;

    }

    /**
     * Returns whether or not the given player has a marble on the board.
     *
     * @param player the player
     * @return whether or not the given player has a marble on the board
     */
    protected static boolean hasMarbleOnBoard(Player player) {
        Preconditions.checkNotNull(player, "Null player");
        return getNumberOfMarblesOnBoard(player) > 0;
    }

    /**
     * Returns whether or not the game is over.
     *
     * @param gameState the game state
     * @return whether or not the game is over
     */
    public static boolean isGameOver(GameState gameState) {
        return getWinningPlayerNumber(gameState) != -1;
    }

    /**
     * Returns whether or not the given marble is on the board.
     *
     * @param marble the marble
     * @return whether or not the given marble is on the board
     */
    protected static boolean isMarbleOnBoard(Marble marble) {
        Preconditions.checkNotNull(marble, "Null marble");
        return marble.getBoardIndex() < Board.BOARD_INDEX_START_MIN[marble.getPlayerNumber()];
    }

    /**
     * Returns whether or not the given marble is in a home space.
     *
     * @param marble the marble
     * @return whether or not the given marble is in a home space
     */
    protected static boolean isMarbleInHomeSpace(Marble marble) {
        Preconditions.checkNotNull(marble, "Null marble");
        return marble.getBoardIndex() >= Board.BOARD_INDEX_HOME_MIN[marble.getPlayerNumber()]
                && marble.getBoardIndex() <= Board.BOARD_INDEX_HOME_MAX[marble.getPlayerNumber()];
    }

    /**
     * Returns whether or not the given marble is in the safe space.
     *
     * @param marble the marble
     * @return whether or not the given marble is in the safe space
     */
    protected static boolean isMarbleInSafeSpace(Marble marble) {
        Preconditions.checkNotNull(marble, "Null marble");
        return marble.getBoardIndex() == Board.BOARD_INDEX_SAFE[marble.getPlayerNumber()];
    }

    /**
     * Returns whether or not the given marble is in a start space.
     *
     * @param marble the marble
     * @return whether or not the given marble is in a start space
     */
    protected static boolean isMarbleInStartSpace(Marble marble) {
        Preconditions.checkNotNull(marble, "Null marble");
        return marble.getBoardIndex() >= Board.BOARD_INDEX_START_MIN[marble.getPlayerNumber()]
                && marble.getBoardIndex() <= Board.BOARD_INDEX_START_MAX[marble.getPlayerNumber()];
    }

    /**
     * Returns whether or not the given player has a marble in the space with
     * the given board index.
     *
     * @param player the player
     * @param boardIndex the board index of the space
     * @return whether or not the given player has a marble in the space with
     * the given board index
     */
    protected static boolean isPlayerMarbleInSpace(Player player, int boardIndex) {

        Preconditions.checkNotNull(player, "Null player");

        for (Marble marble : player.getMarbles()) {
            if (marble.getBoardIndex() == boardIndex) {
                return true;
            }
        }

        return false;

    }

    /**
     * Returns whether or not we need to deal new cards to each player.
     *
     * @param gameState the game state
     * @return whether or not we need to deal new cards to each player
     */
    public static boolean needToDealNewCards(GameState gameState) {

        Preconditions.checkNotNull(gameState, "Null game state");

        boolean playerHasCards = false;

        for (Player player : gameState.getPlayers()) {
            if (!player.getCards().isEmpty()) {
                playerHasCards = true;
                break;
            }
        }

        return !playerHasCards;

    }

}
